package com.young.ums.service.impl;

import com.young.common.exception.BaseException;
import com.young.common.exception.ParamException;
import com.young.common.util.CommonUtil;
import com.young.common.util.IPUtil;
import com.young.common.util.StringUtils;
import com.young.interfaces.log.annotation.Log;
import com.young.interfaces.ums.model.SsoVerifyDTO;
import com.young.ums.model.*;
import com.young.ums.service.*;
import com.young.ums.util.SecureHandler;
import com.young.ums.util.UmsConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;

/**
 * 单点登录服务
 * @author imrookie
 * @date 2018/10/1
 */
@Service("ssoService")
public class SsoServiceImpl implements ISsoService {

    private static final Logger logger = LoggerFactory.getLogger(SsoServiceImpl.class);

    @Resource(name="umsConfigService")
    IUmsConfigService configService;

    @Resource(name="ssoSessionService")
    ISsoSessionService ssoSessionService;

    @Resource(name="userService")
    IUserService userService;

    @Resource(name="loginLogService")
    ILoginLogService loginLogService;
    /**
     * ums缓存服务
     */
    @Autowired
    IUmsCacheService umsCacheService;

    /**
     * 添加登录失败记录
     * @param account
     */
    private void addLoginFailRecord(String account){
        //失败记录
        List<Long> failList = umsCacheService.getSsoFailList(account);
        if (failList == null) {
            failList = new ArrayList<Long>();
        }

        //记录这次的失败时间
        failList.add(System.currentTimeMillis());

        //判断是否达到封禁要求, 若达到,则放入冻结名单

        //去掉30分钟之前的失败记录
        Iterator<Long> it = failList.iterator();
        while (it.hasNext()) {
            Long failTime = it.next();
            if (failTime == null || (System.currentTimeMillis() - failTime) > 30L * 60L * 1000L) {//失败时间距离现在超过30分钟的,移除掉
                it.remove();
                continue;
            }
        }
        //筛掉30分钟前的记录后若还有失败记录,则继续验证
        if (failList != null && !failList.isEmpty()) {
            //查询失败次数阈值
            String numStr = configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_CONTINUOUS_FAIL_MAX_NUM);
            if (StringUtils.isNotBlank(numStr)) {
                int num = Integer.parseInt(numStr);
                if (num > 0 && failList.size() >= num) {//超出限制则放入冻结名单
                    //查询冻结时间,单位s
                    String banTimeStr = configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_CONTINUOUS_FAIL_BAN_SECONDS);
                    Long banTime = null;//单位ms
                    if (StringUtils.isNotBlank(banTimeStr)) {
                        banTime = Long.parseLong(banTimeStr) * 1000L;
                    } else {
                        //配置无效时使用默认时间
                        banTime = 30L * 60L * 1000L;
                    }
                    long endTime = System.currentTimeMillis() + banTime;
                    //记录冻结结束时间
                    umsCacheService.setAccountFreeze(account, endTime);
                    logger.warn("[SSO单点登录] 账号[{}]30分钟内失败次数为[{}],大于阈值[{}],对该账号执行冻结,结束时间为:{}", account, failList.size(), num, endTime);
                } else {
                    logger.debug("[SSO单点登录] 账号[{}]30分钟内失败次数为[{}],阈值为[{}],未达到冻结标准", account, failList.size(), num);
                }
            }
        }

        //更新缓存
        umsCacheService.setSsoFailList(account, failList);
    }

    /**
     * 判断是否封禁该账号, 正常则返回true
     * @param account
     * @return
     */
    private boolean checkBanAccountList(String account){
        //检查封禁名单
        Long banEndTime = umsCacheService.getAccountFreeze(account);//获取账号封禁时间
        if (banEndTime != null) {
            if (System.currentTimeMillis() < banEndTime) {//当前时间小于封禁时间,则返回false
                return false;
            } else {
                //冻结时间已到,从冻结名单去掉该账号
                umsCacheService.removeAccountFreeze(account);
            }
        }
        return true;
    }

    /**
     * 清空失败记录, 登录成功时调用
     * @param account
     */
    private void clearLoginFailRecordAfterLoginSuccess(String account){
        umsCacheService.removeSsoFailList(account);
    }

    /**
     * 单点验证
     * 20181117将sso下发jwt改为直接下发uuid,故修改此方法
     *
     * @param token
     * @return
     */
    @Override
    public SsoVerifyDTO verifySSO(String token) {
        /*//解析令牌
        SsoJWT ssoJWT = null;
        try {
            ssoJWT = SecureHandler.parseJWT(token, configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_RSA_KEY_PRIVATE), configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_HS256_KEY));
        } catch (Exception e) {
            logger.error("[单点登录服务-单点验证] 令牌解析出错,token={}", token, e);
            throw new BaseException("令牌解析失败");
        }

        //查询服务端回话信息,根据jwt的uuid
        SsoSession ssoSession = ssoSessionService.getByToken(ssoJWT.getPayload().getUuid());

        if (ssoSession == null){
            logger.error("[token验证] 根据令牌UUID[{}]在服务端查找失败,用户id为:{}", ssoJWT.getPayload().getUuid(), ssoJWT.getPayload().getUserId());
            return SsoVerifyDTO.buildFail(token, "验证失败");
        }

        if (UmsConstants.SsoSessionStatus.NO == ssoSession.getStatus()){//状态为禁用
            logger.warn("[token验证] 令牌[UUID={}]状态为禁用,用户id为:{}", ssoJWT.getPayload().getUuid(), ssoJWT.getPayload().getUserId());
            return SsoVerifyDTO.buildFail(token, "验证失败");
        }

        if (ssoSession.getExpireTime().before(new Date())){//已经过期
            logger.info("[token验证] 令牌已过期,令牌UUID={},用户id={}", ssoJWT.getPayload().getUuid(), ssoJWT.getPayload().getUserId());
            return SsoVerifyDTO.buildFail(token, "会话已过期");
        }*/

        //查询服务端会话信息
        SsoSession ssoSession = ssoSessionService.getByToken(token);
        if (ssoSession == null){
            logger.error("[token验证] 根据令牌[{}]在服务端查找失败", token);
            return SsoVerifyDTO.buildFail(token, "验证失败");
        }

        if (UmsConstants.SsoSessionStatus.NO == ssoSession.getStatus()){//状态为禁用
            logger.warn("[token验证] 令牌={},状态为禁用,用户id={}", token, ssoSession.getUserId());
            return SsoVerifyDTO.buildFail(token, "验证失败");
        }

        if (ssoSession.getExpireTime().before(new Date())){//已经过期
            logger.info("[token验证] 令牌已过期,令牌={},用户id={}", token, ssoSession.getUserId());
            return SsoVerifyDTO.buildFail(token, "会话已过期");
        }

        User user = userService.get(ssoSession.getUserId());//查询用户信息

        if (user == null){
            logger.warn("[token验证] 根据用户id查询用户信息失败,用户id为:{}", ssoSession.getUserId());
            return SsoVerifyDTO.buildFail(token, "验证失败");
        }

        user.setToken(token);
        return SsoVerifyDTO.buildSuccess(token, user.convert2User());
    }

    /**
     * sso
     *
     * @param account  账号
     * @param password 密码
     * @param clientIp 客户端ip
     * @param agent 设备类型
     * @return token
     */
    @Override
    @Log("登录验证")
    public String sso(String account, String password, String clientIp, String agent) throws Exception {
        String token = null;//令牌
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)) {
            throw new BaseException("账号密码无效");
        }
        //对用户的IP进行过滤
        String blacklist = configService.getUsableConfigValue(UmsConstants.UmsConfigKey.LOGIN_IP_BLACK_LIST);//从配置项中拉取黑名单
        if (blacklist != null && StringUtils.isNotBlank(blacklist)){
            if (!this.verifyIp(clientIp, blacklist)){//ip验证不通过
                writeLoginLog(account, UmsConstants.LoginLogResult.NO, "IP黑名单", clientIp, null);//记录登录日志
                throw new BaseException("登录失败");
            }
        }

        //校验冻结名单
        if (!this.checkBanAccountList(account)) {
            writeLoginLog(account, UmsConstants.LoginLogResult.NO, "失败次数过多", clientIp, null);//记录登录日志
            throw new BaseException("失败次数过多,已被冻结登录");
        }

        User user = userService.getUserByAccount(account);
        if(user == null){
            writeLoginLog(account, UmsConstants.LoginLogResult.NO, "用户名不存在", clientIp, null);//记录登录日志
            throw new BaseException("用户名不存在");
        }
        if(!verifyUser(account, password, user)){//验证用户
            logger.info("[SSO单点登录] 用户名或密码错误,account={},password={}", account, password);
            writeLoginLog(account, UmsConstants.LoginLogResult.NO, "用户名或密码错误", clientIp, null);//记录登录日志
            //记录该账号失败记录
            this.addLoginFailRecord(account);
            throw new BaseException("用户名或密码错误");
        }else{
            //验证成功,生成jwt令牌
            user.setPassword("");//清空密码

            //创建jwt
            SsoJWT ssoJWT = this.createJWT(user);
            //目前不使用下发jwt方式实现sso,下发uuid(更快更省)
            token = ssoJWT.getPayload().getUuid();
            user.setToken(token);


            //记录当前会话
            SsoSession ssoSession = new SsoSession();
            ssoSession.setUserId(user.getId());
            ssoSession.setDeviceType(agent);
            ssoSession.setDeviceVersion("");
            ssoSession.setToken(ssoJWT.getPayload().getUuid());//jwt经加密后长度为1024,不方便存储,这里存储jwt的uuid
            ssoSession.setSignTime(ssoJWT.getPayload().getIat());
            ssoSession.setExpireTime(ssoJWT.getPayload().getExp());
            ssoSession.setClientIp(clientIp);
            ssoSession.setServerIp(IPUtil.getLocalIp());
            ssoSession.setStatus(UmsConstants.SsoSessionStatus.YES);
            ssoSessionService.insert(ssoSession);

            logger.info("[SSO单点登录] 用户[" + account + "]登录验证通过:{}", user);

            writeLoginLog(account, UmsConstants.LoginLogResult.YES, "登录成功", clientIp, ssoSession);//记录登录日志
            userService.updateLoginTime(user.getId(), ssoSession.getSignTime());//更新登录时间

            //登录成功后清空失败记录
            this.clearLoginFailRecordAfterLoginSuccess(account);
        }
        //校验token
        if (StringUtils.isBlank(token)) {
            logger.error("[SSO单点登录] 意料之外的情况,token无效...");
            writeLoginLog(account, UmsConstants.LoginLogResult.NO, "token无效", clientIp, null);//记录登录日志
            throw new BaseException("登录失败,请联系管理员!");
        }
        return token;
    }

    /**
     * 注册
     *
     * @param user 用户信息
     */
    @Log("用户注册-Service")
    @Override
    public void register(User user) {
        //参数校验
        if (user == null || StringUtils.isBlank(user.getAccount()) || StringUtils.isBlank(user.getPassword())) {
            throw new ParamException("参数异常!");
        }
        //判断账号是否被使用
        User searchParam = new User();
        searchParam.setAccount(user.getAccount());
        //根据账号查询用户数
        int num = userService.count(searchParam);
        if (num > 0) {
            throw new ParamException("账号已存在!");
        }

        //判断该账号作为id是否被使用
        searchParam.setAccount(null);
        searchParam.setId(user.getAccount());
        num = userService.count(searchParam);
        if (num > 0) {//id已存在就new一个
            user.setId(CommonUtil.getUUID());
        } else {//id没被占用就使用账号作为id
            user.setId(user.getAccount());
        }

        logger.info("[用户注册] 输入信息有效,进行用户插入操作.user={}", user);

        //插入
        userService.insert(user);
    }

    /**
     * 记录登录日志
     * @param account 账号
     * @param result 登录结果,UmsConstants.LoginLogResult
     * @param remark 说明
     * @param ssoSession 登录成功后的会话信息
     */
    private void writeLoginLog(String account, String result, String remark, String clientIp, SsoSession ssoSession){
        try{
            loginLogService.writeLoginLog(account, result, remark, clientIp, ssoSession);
        }catch (Exception e){
            logger.error("[记录登录日志] 发生异常,账号={},登录结果={}", account, result, e);
        }
    }

    /**
     * 验证ip
     * @param ip 待验证的ip
     * @param blacklist 黑名单
     * @return
     */
    private boolean verifyIp(String ip, String blacklist){
        if (StringUtils.isNotBlank(ip) && StringUtils.isNotBlank(blacklist)){
            String[] ips = blacklist.split(";");
            if (ips != null && ips.length > 0){
                for (String black : ips){
                    black = black.trim();//去除空白
                    if (StringUtils.isNotBlank(black)){
                        String[] blackItems = black.split("\\.");
                        String[] ipItems = ip.split("\\.");
                        if (blackItems.length != 0 && blackItems.length == ipItems.length){//ip格式相同
                            boolean flag = true;
                            for (int i=0;i<blackItems.length;i++){
                                if ("*".equals(blackItems[i]) || StringUtils.equals(blackItems[i], ipItems[i])){//通配符配置，该段忽略//该段相等，则继续下一段匹配
                                    continue;
                                } else {//不相等，则跳出匹配，进行下一个黑ip的匹配
                                    flag = false;
                                    break;
                                }
                            }
                            if (flag){//匹配成功，说明当前IP为黑名单中的IP
                                logger.warn("[SSO单点登录] IP验证失败，可疑的用户IP：{}，对应黑名单IP为：{}", ip, black);
                                //返回验证失败
                                return false;
                            }
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * 验证用户
     * 验证规则:
     * 1.数据库中存在对于账号的用户信息;
     * 2.用户状态为可用;
     * 3.输入的账号密码与数据库查询到的一致;
     * @param account 用户输入的账号
     * @param password 用户输入的密码
     * @param user 数据库中查询到的用户信息
     * @return
     */
    private boolean verifyUser(String account,String password,User user){
        try {
            if(user != null && UmsConstants.UserState.YES == user.getState() && SecureHandler.encryptPwd(password).equals(user.getPassword()) && account.equals(user.getAccount())){//密码匹配验证
                return true;
            }
        } catch (Exception e) {
            logger.error("[SSO单点登录] 用户验证阶段发生异常,用户信息:{}", user, e);
        }
        return false;
    }

    /**
     * 创建JWT
     * @param user
     * @return
     * @throws Exception
     */
    private SsoJWT createJWT(User user) throws Exception {
        //为用户发放令牌,令牌信息通过SecureHandler类生成
        int validTime = UmsConstants.DEFAULT.TOKEN_VALID_TIMES;//默认有效时间
        try{
            String validTimeCfg = configService.getUsableConfigValue(UmsConstants.UmsConfigKey.TOKEN_VALID_TIME);//令牌有效期配置
            if (StringUtils.isNotBlank(validTimeCfg)){
                validTime = Integer.valueOf(validTimeCfg);
            }
        }catch (Exception e){
            logger.error("[SSO单点登录] 设置TOKEN有效时间发生异常,降级为使用默认值:{}", validTime, e);
        }

        //生产jwt
        SsoJWT ssoJWT = SecureHandler.generateJWT(user, validTime, configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_HS256_KEY));
        return ssoJWT;
    }

    /**
     * 创建用于签发给客户端的jwt
     * @param user
     * @return
     * @throws Exception
     */
    private String createJWTAsString(User user) throws Exception {
        //创建jwt
        SsoJWT ssoJWT = this.createJWT(user);
        //签发jwt
        String token = SecureHandler.signJWT(ssoJWT, configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_RSA_KEY_PUBLIC));
        return token;
    }
}
